package cn.iocoder.yudao.module.system.service.book;

import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.infra.service.file.FileService;
import cn.iocoder.yudao.module.infra.service.file.FileConfigService;
import cn.iocoder.yudao.module.system.service.file.FileProcessingService;
import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClient;
import cn.iocoder.yudao.module.system.dal.dataobject.book.BookDO;
import cn.iocoder.yudao.module.system.dal.dataobject.book.BookPageDO;
import cn.iocoder.yudao.module.system.dal.mysql.book.BookMapper;
import cn.iocoder.yudao.module.system.dal.mysql.book.BookPageMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import javax.validation.Valid;
import java.io.*;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.TimeUnit;

// 新增解压相关的导入
import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.ArchiveInputStream;
import org.apache.commons.compress.archivers.ArchiveStreamFactory;
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
import org.apache.commons.compress.archivers.sevenz.SevenZFile;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.regex.Pattern;
import java.util.regex.Matcher;

import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;

/**
 * 绘本上传 Service 实现类
 *
 * @author 芋道源码
 */
@Service
@Validated
@Slf4j
public class BookUploadServiceImpl implements BookUploadService {

    @Resource
    private BookMapper bookMapper;

    @Resource
    private BookPageMapper bookPageMapper;

    @Resource
    private FileService fileService;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private FileConfigService fileConfigService;

    @Resource
    private FileProcessingService fileProcessingService;

    // Redis键前缀
    private static final String SESSION_TEMP_KEYS_PREFIX = "session:";
    private static final String SESSION_PROCESSED_PREFIX = "processed:";

    // 文件配置
    private static final int TEMP_URL_EXPIRE_MINUTES = 15;
    private static final int TEMP_KEY_EXPIRE_MINUTES = 15;
    private static final int PROCESSED_EXPIRE_HOURS = 24;

    // 文件大小限制
    private static final int MAX_COMPRESSED_FILE_SIZE_MB = 100;
    private static final int MAX_COVER_FILE_SIZE_MB = 5;

    // 允许的文件类型
    private static final String[] ALLOWED_COMPRESSED_TYPES = {"zip", "rar", "7z"};
    private static final String[] ALLOWED_COVER_TYPES = {"jpg", "jpeg", "png"};

    // 定时清理配置
    private static final int TEMP_FILE_EXPIRE_MINUTES = 30; // 临时文件过期时间（分钟）
    private static final int CLEANUP_BATCH_SIZE = 100; // 批量清理大小
    private static final int CLEANUP_MAX_SESSIONS = 1000; // 单次清理最大会话数

    @Override
    public Map<String, String> uploadCompressedFile(MultipartFile file, String sessionUuid) throws IOException {
        logOperation("上传压缩包", sessionUuid, file.getOriginalFilename());

        try {
            // 1. 校验文件格式和大小
            if (!validateFile(file, ALLOWED_COMPRESSED_TYPES, MAX_COMPRESSED_FILE_SIZE_MB)) {
                throw exception(BOOK_UPLOAD_FILE_FORMAT_ERROR);
            }

            // 2. 生成唯一文件名
            String uniqueFilename = generateUniqueFilename(file.getOriginalFilename(), sessionUuid);
            String tempPath = buildTempPath(sessionUuid, uniqueFilename);

            // 3. 上传到MinIO临时路径
            String fileUrl = fileService.createFile(file.getOriginalFilename(), tempPath, file.getBytes());

            // 4. 生成tempKey
            String tempKey = generateTempKey(sessionUuid, "compressed");

            // 5. 存储到Redis
            storeTempKey(sessionUuid, tempKey, "compressed", tempPath);

            // 6. 生成临时URL
            String minioObjectUrl = generateTempUrl(tempPath, TEMP_URL_EXPIRE_MINUTES);

            Map<String, String> result = new HashMap<>();
            result.put("minioObjectUrl", minioObjectUrl);
            result.put("tempKey", tempKey);
            result.put("filePath", tempPath);

            logOperation("压缩包上传成功", sessionUuid, tempPath, tempKey);
            return result;

        } catch (Exception e) {
            logError("压缩包上传失败", sessionUuid, e);
            throw e;
        }
    }

    @Override
    public Map<String, String> uploadCoverFile(MultipartFile file, String sessionUuid) throws IOException {
        logOperation("上传封面", sessionUuid, file.getOriginalFilename());

        try {
            // 1. 校验文件格式和大小
            if (!validateFile(file, ALLOWED_COVER_TYPES, MAX_COVER_FILE_SIZE_MB)) {
                throw exception(BOOK_UPLOAD_FILE_FORMAT_ERROR);
            }

            // 2. 校验会话UUID一致性（检查是否已有压缩包上传）
            Map<String, Object> tempKeyInfo = getTempKeyInfo(sessionUuid);
            if (tempKeyInfo == null || tempKeyInfo.get("compressed") == null) {
                throw exception(BOOK_UPLOAD_SESSION_INVALID);
            }

            // 3. 生成唯一文件名
            String uniqueFilename = generateUniqueFilename(file.getOriginalFilename(), sessionUuid);
            String tempPath = buildTempPath(sessionUuid, uniqueFilename);

            // 4. 上传到MinIO临时路径
            String fileUrl = fileService.createFile(file.getOriginalFilename(), tempPath, file.getBytes());

            // 5. 生成tempKey
            String tempKey = generateTempKey(sessionUuid, "cover");

            // 6. 存储到Redis
            storeTempKey(sessionUuid, tempKey, "cover", tempPath);

            // 7. 生成临时URL
            String coverUrl = generateTempUrl(tempPath, TEMP_URL_EXPIRE_MINUTES);

            Map<String, String> result = new HashMap<>();
            result.put("coverUrl", coverUrl);
            result.put("tempKey", tempKey);
            result.put("filePath", tempPath);

            logOperation("封面上传成功", sessionUuid, tempPath, tempKey);
            return result;

        } catch (Exception e) {
            logError("封面上传失败", sessionUuid, e);
            throw e;
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long submitBookUpload(String title, String sessionUuid, String compressedTempKey,
                                 String coverTempKey, String minioObjectUrl, String coverUrl,
                                 String formatDescription) {
        logOperation("开始最终提交处理", sessionUuid, title);
        
        Long bookId = null;
        // 转存文件列表
        List<String> transferredFiles = new ArrayList<>();
        // 临时目录路径
        String tempDir = null;
        
        try {
            // 1. 前置校验（Redis查询、tempKey验证、会话一致性检查）
            Map<String, Object> tempKeyInfo = getTempKeyInfo(sessionUuid);
            if (tempKeyInfo == null) {
                throw exception(BOOK_UPLOAD_SESSION_EXPIRED);
            }
            
            // 校验tempKey
            if (!validateTempKey(sessionUuid, compressedTempKey, "compressed")) {
                throw exception(BOOK_UPLOAD_TEMP_KEY_INVALID);
            }
            if (!validateTempKey(sessionUuid, coverTempKey, "cover")) {
                throw exception(BOOK_UPLOAD_TEMP_KEY_INVALID);
            }
            
            // 2. 校验绘本名称
            Map<String, Object> titleValidation = validateBookTitle(title);
            if (!(Boolean) titleValidation.get("valid")) {
                throw exception(BOOK_UPLOAD_TITLE_INVALID);
            }
            
            // 3. 检查幂等性
            if (checkIdempotency(sessionUuid)) {
                logOperation("检测到重复提交，返回已处理结果", sessionUuid);
                // 返回已处理的bookId（这里需要从Redis获取）
                String processedBookId = stringRedisTemplate.opsForValue().get(buildProcessedSessionKey(sessionUuid));
                if (processedBookId != null) {
                    return Long.valueOf(processedBookId);
                }
            }
            
            // 4. 标记会话为处理中（防止并发提交）
            if (!markSessionAsProcessing(sessionUuid)) {
                throw exception(BOOK_UPLOAD_SESSION_INVALID);
            }
            
            // 5. 开启数据库事务（@Transactional注解已处理）
            
            // 5. 插入huiben_book表
            BookDO book = new BookDO();
            book.setTitle(title);
            book.setCreatedAt(LocalDateTime.now());
            bookMapper.insert(book);
            bookId = book.getId();
            
            logOperation("创建绘本记录", sessionUuid, "bookId: " + bookId);
            
            // 6. 文件转存与处理
            // 6.1 封面转存
            String compressedPath = (String) tempKeyInfo.get("compressed_path");
            String coverPath = (String) tempKeyInfo.get("cover_path");
            
            String coverPermanentPath = buildCoverPermanentPath(bookId);
            if (!transferFileWithRollback(coverPath, coverPermanentPath, transferredFiles)) {
                throw exception(BOOK_UPLOAD_STORAGE_FAILED);
            }
            
            // 6.2 压缩包处理
            String compressedPermanentPath = buildPermanentPath(bookId, "resource.zip");
            if (!transferFileWithRollback(compressedPath, compressedPermanentPath, transferredFiles)) {
                throw exception(BOOK_UPLOAD_STORAGE_FAILED);
            }
            
            // 6.3 解压和校验压缩包内容
            Map<String, Object> extractedFiles = extractAndValidateCompressedFile(compressedPermanentPath);
            if (extractedFiles == null || extractedFiles.isEmpty()) {
                throw exception(BOOK_UPLOAD_COMPRESSED_CONTENT_ERROR);
            }
            
            // 获取临时目录路径
            tempDir = (String) extractedFiles.get("tempDir");
            
            // 6.4 将解压后的文件上传到MinIO正式目录
            @SuppressWarnings("unchecked")
            List<Map<String, Object>> validPages = (List<Map<String, Object>>) extractedFiles.get("validPages");
            List<BookPageDO> bookPages = new ArrayList<>();
            
            for (Map<String, Object> page : validPages) {
                Integer pageNum = (Integer) page.get("pageNum");
                String tempImagePath = (String) page.get("imageUrl");
                String tempAudioPath = (String) page.get("audioUrl");
                Integer duration = (Integer) page.get("duration");
                
                log.info("处理页面 {}: 图片路径={}, 音频路径={}, 时长={}", pageNum, tempImagePath, tempAudioPath, duration);
                
                // 构建正式目录路径
                String imageFileName = getFileNameFromPath(tempImagePath);
                String imageExtension = getFileExtension(imageFileName);
                String imagePermanentPath = "books/" + bookId + "/pages/" + pageNum + "/image." + imageExtension;
                
                String audioPermanentPath = null;
                if (tempAudioPath != null) {
                    String audioFileName = getFileNameFromPath(tempAudioPath);
                    String audioExtension = getFileExtension(audioFileName);
                    audioPermanentPath = "books/" + bookId + "/pages/" + pageNum + "/audio." + audioExtension;
                    log.info("页面 {} 音频文件: 原始路径={}, 文件名={}, 扩展名={}, 永久路径={}", 
                            pageNum, tempAudioPath, audioFileName, audioExtension, audioPermanentPath);
                } else {
                    log.warn("页面 {} 没有音频文件", pageNum);
                }
                
                // tempImagePath 和 tempAudioPath 已经是完整的临时文件路径
                // 上传图片文件到MinIO
                if (!uploadExtractedFileToMinio(tempImagePath, imagePermanentPath, transferredFiles)) {
                    throw exception(BOOK_UPLOAD_STORAGE_FAILED);
                }
                // 上传音频文件到MinIO（仅当 audioPermanentPath 不为 null 时）
                if (audioPermanentPath != null) {
                    log.info("开始上传页面 {} 的音频文件到: {}", pageNum, audioPermanentPath);
                    if (!uploadExtractedFileToMinio(tempAudioPath, audioPermanentPath, transferredFiles)) {
                        log.error("页面 {} 音频文件上传失败", pageNum);
                        throw exception(BOOK_UPLOAD_STORAGE_FAILED);
                    }
                    log.info("页面 {} 音频文件上传成功", pageNum);
                }
                
                // 创建数据库记录
                BookPageDO bookPage = new BookPageDO();
                bookPage.setBookId(bookId);
                bookPage.setPageNum(pageNum);
                bookPage.setImageUrl(imagePermanentPath);
                if (audioPermanentPath != null) {
                    bookPage.setAudioUrl(audioPermanentPath);
                    log.info("页面 {} 设置音频URL: {}", pageNum, audioPermanentPath);
                } else {
                    log.warn("页面 {} 音频URL为null", pageNum);
                }
                bookPage.setDuration(duration);
                bookPages.add(bookPage);
            }
            
            if (!bookPages.isEmpty()) {
                // 批量插入页面
                for (BookPageDO bookPage : bookPages) {
                    bookPageMapper.insert(bookPage);
                }
            }
            
            // 8. 更新huiben_book表
            
            book.setCoverUrl(coverPermanentPath); // 永久URL
            book.setTotalpage(String.valueOf(validPages.size()));
            bookMapper.updateById(book);
            
            // 9. 标记会话为已处理
            String processedKey = buildProcessedSessionKey(sessionUuid);
            stringRedisTemplate.opsForValue().set(processedKey, String.valueOf(bookId), PROCESSED_EXPIRE_HOURS, TimeUnit.HOURS);
            logOperation("标记会话为已处理", sessionUuid, "bookId: " + bookId);
            
            // 10. 删除Redis临时数据
            deleteTempKeyInfo(sessionUuid);
            
            // 11. 删除整个临时文件夹（所有文件已转存完成）
            if (tempDir != null) {
                deleteTempDirectoryCompletely(tempDir);
            }
            
            logOperation("最终提交处理完成", sessionUuid, "bookId: " + bookId);
            return bookId;
            
        } catch (Exception e) {
            logError("最终提交处理失败", sessionUuid, e);
            
            // 异常回滚处理
            handleSubmitRollback(bookId, transferredFiles, sessionUuid, e);
            
            // 清除处理中标记
            clearProcessingMark(sessionUuid);
            
            throw e;
        } finally {
            // 确保清除处理中标记
            clearProcessingMark(sessionUuid);
            
            // 在异常情况下也清理临时目录
            if (tempDir != null) {
                try {
                    deleteTempDirectoryCompletely(tempDir);
                } catch (Exception e) {
                    logError("finally块中清理临时目录失败", sessionUuid, e);
                }
            }
        }
    }

    @Override
    public void cleanupExpiredTempFiles() {
        logOperation("开始定时清理过期临时文件", "SYSTEM", "开始时间: " + LocalDateTime.now());
        
        try {
            // 1. 扫描books/temp/目录
            List<String> expiredSessionUuids = scanExpiredTempDirectories();
            
            if (expiredSessionUuids.isEmpty()) {
                logOperation("定时清理完成", "SYSTEM", "没有发现过期的临时文件");
                return;
            }
            
            logOperation("发现过期会话", "SYSTEM", "过期会话数量: " + expiredSessionUuids.size());
            
            // 2. 删除过期的会话UUID目录和文件
            int deletedFilesCount = deleteExpiredTempFiles(expiredSessionUuids);
            
            // 3. 同步清理Redis中对应会话的tempKey数据
            int deletedRedisCount = cleanupExpiredRedisData(expiredSessionUuids);
            
            logOperation("定时清理完成", "SYSTEM", 
                String.format("删除文件: %d, 清理Redis: %d, 过期会话: %d", 
                    deletedFilesCount, deletedRedisCount, expiredSessionUuids.size()));
            
        } catch (Exception e) {
            logError("定时清理失败", "SYSTEM", e);
        }
    }

    @Override
    public boolean validateSessionUuid(String sessionUuid) {
        if (sessionUuid == null || sessionUuid.trim().isEmpty()) {
            return false;
        }

        // 检查UUID格式
        try {
            UUID.fromString(sessionUuid);
        } catch (IllegalArgumentException e) {
            return false;
        }

        // 检查是否在Redis中存在
        String redisKey = buildSessionTempKeysKey(sessionUuid);
        Boolean hasKey = stringRedisTemplate.hasKey(redisKey);
        return Boolean.TRUE.equals(hasKey);
    }

    @Override
    public boolean validateTempKey(String sessionUuid, String tempKey, String type) {
        if (sessionUuid == null || tempKey == null || type == null) {
            return false;
        }

        try {
            // 1. 从Redis获取tempKey信息
            Map<String, Object> tempKeyInfo = getTempKeyInfo(sessionUuid);
            if (tempKeyInfo == null) {
                return false;
            }

            // 2. 校验tempKey是否匹配
            String storedTempKey = (String) tempKeyInfo.get(type);
            if (!tempKey.equals(storedTempKey)) {
                return false;
            }

            // 3. 检查是否在有效期内（通过Redis的过期时间自动控制）
            return true;

        } catch (Exception e) {
            logError("tempKey校验失败", sessionUuid, e);
            return false;
        }
    }

    @Override
    public String generateTempUrl(String path, int expireMinutes) {
        try {
            // FileService的getFilePresignedUrl方法不接收过期时间参数，使用默认过期时间
            return fileService.getFilePresignedUrl(path).getUrl();
        } catch (Exception e) {
            logError("生成临时URL失败", "N/A", e);
            return null;
        }
    }

    @Override
    public String generateTempKey(String sessionUuid, String type) {
        long timestamp = System.currentTimeMillis();
        String uuid = UUID.randomUUID().toString().replace("-", "");
        String sign = generateSign(sessionUuid, type, timestamp);

        return type + "_" + uuid + "_" + timestamp + "_" + sign;
    }

    @Override
    public void storeTempKey(String sessionUuid, String tempKey, String type, String minioPath) {
        String redisKey = buildSessionTempKeysKey(sessionUuid);

        Map<String, String> hashData = new HashMap<>();
        hashData.put(type, tempKey);
        hashData.put(type + "_path", minioPath);
        hashData.put(type + "_timestamp", String.valueOf(System.currentTimeMillis()));

        stringRedisTemplate.opsForHash().putAll(redisKey, hashData);
        stringRedisTemplate.expire(redisKey, TEMP_KEY_EXPIRE_MINUTES, TimeUnit.MINUTES);

        logOperation("存储tempKey", sessionUuid, type, tempKey);
    }

    @Override
    public Map<String, Object> getTempKeyInfo(String sessionUuid) {
        String redisKey = buildSessionTempKeysKey(sessionUuid);
        Map<Object, Object> hashData = stringRedisTemplate.opsForHash().entries(redisKey);

        if (hashData.isEmpty()) {
            return null;
        }

        Map<String, Object> result = new HashMap<>();
        for (Map.Entry<Object, Object> entry : hashData.entrySet()) {
            result.put(entry.getKey().toString(), entry.getValue());
        }

        return result;
    }

    @Override
    public void deleteTempKeyInfo(String sessionUuid) {
        String redisKey = buildSessionTempKeysKey(sessionUuid);
        stringRedisTemplate.delete(redisKey);

        // 同时删除已处理标记
        String processedKey = buildProcessedSessionKey(sessionUuid);
        stringRedisTemplate.delete(processedKey);

        logOperation("删除tempKey信息", sessionUuid);
    }

    @Override
    public boolean validateFile(MultipartFile file, String[] allowedTypes, int maxSizeMB) {
        // 1. 检查文件是否为空
        if (file == null || file.isEmpty()) {
            log.warn("文件为空");
            return false;
        }

        // 2. 检查文件大小
        long fileSizeMB = file.getSize() / (1024 * 1024);
        if (fileSizeMB > maxSizeMB) {
            log.warn("文件大小超过限制: {}MB > {}MB", fileSizeMB, maxSizeMB);
            return false;
        }

        // 3. 检查文件类型
        String originalFilename = file.getOriginalFilename();
        if (originalFilename == null || originalFilename.isEmpty()) {
            log.warn("文件名为空");
            return false;
        }

        String fileExtension = getFileExtension(originalFilename);
        if (fileExtension == null) {
            log.warn("无法获取文件扩展名: {}", originalFilename);
            return false;
        }

        boolean isValidType = false;
        for (String allowedType : allowedTypes) {
            if (allowedType.equalsIgnoreCase(fileExtension)) {
                isValidType = true;
                break;
            }
        }

        if (!isValidType) {
            log.warn("文件类型不支持: {}, 允许的类型: {}", fileExtension, Arrays.toString(allowedTypes));
            return false;
        }

        return true;
    }

    @Override
    public String generateUniqueFilename(String originalFilename, String sessionUuid) {
        String extension = getFileExtension(originalFilename);
        String uuid = UUID.randomUUID().toString().replace("-", "");

        if (extension != null) {
            return uuid + "." + extension;
        } else {
            return uuid;
        }
    }

    @Override
    public Map<String, Object> extractAndValidateCompressedFile(String compressedFilePath) {
        logOperation("开始解压和校验压缩包", "N/A", compressedFilePath);
        
        String tempDir = null;
        try {
            // 1. 创建临时解压目录
            tempDir = System.getProperty("java.io.tmpdir") + "/book_extract_" + System.currentTimeMillis();
            java.io.File tempDirFile = new java.io.File(tempDir);
            if (!tempDirFile.exists()) {
                tempDirFile.mkdirs();
            }
            
            // 2. 获取压缩包文件内容
            byte[] compressedContent = fileService.getFileContent(fileConfigService.getMasterFileClient().getId(), compressedFilePath);
            if (compressedContent == null) {
                throw new Exception("无法获取压缩包内容");
            }
            
            // 3. 解压文件
            List<String> extractedFiles = extractCompressedFile(compressedContent, tempDir, compressedFilePath);
            
            // 4. 验证解压后的文件
            Map<String, Object> result = validateExtractedFiles(extractedFiles, tempDir);
            
            // 5. 记录警告信息
            List<String> warnings = (List<String>) result.get("warnings");
            if (warnings != null && !warnings.isEmpty()) {
                log.warn("压缩包校验发现警告: {}", warnings);
            }
            
            // 6. 添加临时目录路径到结果中
            if (result != null) {
                result.put("tempDir", tempDir);
            }
            
            logOperation("压缩包解压和校验完成", "N/A", 
                String.format("有效页数: %d, 警告数: %d", 
                    ((List<Map<String, Object>>) result.get("validPages")).size(), 
                    warnings != null ? warnings.size() : 0));
            return result;
            
        } catch (Exception e) {
            logError("压缩包解压和校验失败", "N/A", e);
            return null;
        }
        // 注意：临时目录的清理移到调用方处理
    }

    /**
     * 解压压缩文件
     */
    private List<String> extractCompressedFile(byte[] compressedContent, String tempDir, String compressedFilePath) throws Exception {
        List<String> extractedFiles = new ArrayList<>();
        
        // 根据文件扩展名选择解压方式
        String fileExtension = getFileExtension(getFileNameFromPath(compressedFilePath));
        
        if ("zip".equalsIgnoreCase(fileExtension)) {
            extractedFiles = extractZipFile(compressedContent, tempDir);
        } else if ("7z".equalsIgnoreCase(fileExtension)) {
            extractedFiles = extractSevenZFile(compressedContent, tempDir);
        } else {
            throw new Exception("不支持的压缩格式: " + fileExtension);
        }
        
        return extractedFiles;
    }

    /**
     * 解压ZIP文件
     */
    private List<String> extractZipFile(byte[] compressedContent, String tempDir) throws Exception {
        List<String> extractedFiles = new ArrayList<>();
        
        try (InputStream inputStream = new ByteArrayInputStream(compressedContent);
             ArchiveInputStream archiveInputStream = new ArchiveStreamFactory()
                 .createArchiveInputStream("zip", inputStream)) {
            
            ArchiveEntry entry;
            while ((entry = archiveInputStream.getNextEntry()) != null) {
                if (!entry.isDirectory()) {
                    String fileName = entry.getName();
                    java.io.File outputFile = new java.io.File(tempDir, fileName);
                    
                    // 创建父目录
                    outputFile.getParentFile().mkdirs();
                    
                    // 写入文件
                    try (FileOutputStream outputStream = new FileOutputStream(outputFile)) {
                        IOUtils.copy(archiveInputStream, outputStream);
                    }
                    
                    extractedFiles.add(fileName);
                }
            }
        }
        
        // 解压完成后，进行文件压缩和格式转换
        if (!extractedFiles.isEmpty()) {
            log.info("开始处理解压后的文件，总数: {}", extractedFiles.size());
            boolean processingSuccess = fileProcessingService.processExtractedFiles(tempDir, extractedFiles);
            if (!processingSuccess) {
                log.warn("文件处理过程中出现部分失败，但解压操作已完成");
            }
            // 替换 extractedFiles 里的 MP3 为 AAC
            for (int i = 0; i < extractedFiles.size(); i++) {
                String fileName = extractedFiles.get(i);
                if (fileName.toLowerCase().endsWith(".mp3")) {
                    int dotIndex = fileName.lastIndexOf('.');
                    if (dotIndex > 0) {
                        String baseName = fileName.substring(0, dotIndex);
                        String aacName = baseName + ".aac";
                        extractedFiles.set(i, aacName);
                    }
                }
            }
        }
        
        return extractedFiles;
    }

    /**
     * 解压7Z文件
     */
    private List<String> extractSevenZFile(byte[] compressedContent, String tempDir) throws Exception {
        List<String> extractedFiles = new ArrayList<>();
        
        // 先将内容写入临时文件，因为SevenZFile需要文件路径
        java.io.File tempFile = new java.io.File(tempDir, "temp.7z");
        try (FileOutputStream fos = new FileOutputStream(tempFile)) {
            fos.write(compressedContent);
        }
        
        try (SevenZFile sevenZFile = new SevenZFile(tempFile)) {
            ArchiveEntry entry;
            while ((entry = sevenZFile.getNextEntry()) != null) {
                if (!entry.isDirectory()) {
                    String fileName = entry.getName();
                    java.io.File outputFile = new java.io.File(tempDir, fileName);
                    
                    // 创建父目录
                    outputFile.getParentFile().mkdirs();
                    
                    // 写入文件
                    try (FileOutputStream outputStream = new FileOutputStream(outputFile)) {
                        byte[] buffer = new byte[4096];
                        int bytesRead;
                        while ((bytesRead = sevenZFile.read(buffer)) != -1) {
                            outputStream.write(buffer, 0, bytesRead);
                        }
                    }
                    
                    extractedFiles.add(fileName);
                }
            }
        }
        
        // 删除临时文件
        tempFile.delete();
        
        return extractedFiles;
    }

    /**
     * 验证解压后的文件
     */
    private Map<String, Object> validateExtractedFiles(List<String> extractedFiles, String tempDir) {
        Map<String, Object> result = new HashMap<>();
        List<Map<String, Object>> validPages = new ArrayList<>();
        List<String> warnings = new ArrayList<>();
        
        // 添加调试日志
        log.info("解压后的文件列表: {}", extractedFiles);
        log.info("临时目录: {}", tempDir);
        
        // 按页码分组文件
        Map<String, Map<String, String>> pageFiles = new HashMap<>();
        
        for (String fileName : extractedFiles) {
            // 解析文件名格式：01.png, 01.mp3, 02.jpg, 02.wav, 03.gif, 03.aac 等
            String pageNum = extractPageNumber(fileName);
            String fileType = getFileType(fileName);
            
            // 统一页码格式：如01、1都转成"1"
            if (pageNum != null) {
                try {
                    pageNum = String.valueOf(Integer.parseInt(pageNum));
                } catch (NumberFormatException e) {
                    // 保底，非数字页码不处理
                }
            }
            log.info("处理文件: {}, 页码: {}, 文件类型: {}", fileName, pageNum, fileType);
            
            if (pageNum != null && fileType != null) {
                pageFiles.computeIfAbsent(pageNum, k -> new HashMap<>())
                         .put(fileType, fileName);
                log.info("成功添加文件到分组: 页码={}, 类型={}, 文件名={}", pageNum, fileType, fileName);
            } else {
                log.warn("无法解析文件名: {} (页码={}, 类型={})", fileName, pageNum, fileType);
            }
        }
        
        log.info("按页码分组的文件: {}", pageFiles);
        
        // 验证每个页面，并按页码数字排序
        List<String> sortedPageNums = new ArrayList<>(pageFiles.keySet());
        sortedPageNums.sort((a, b) -> {
            try {
                int pageA = Integer.parseInt(a);
                int pageB = Integer.parseInt(b);
                return Integer.compare(pageA, pageB);
            } catch (NumberFormatException e) {
                // 如果页码不是数字，按字符串排序
                return a.compareTo(b);
            }
        });
        
        for (String pageNum : sortedPageNums) {
            Map<String, String> files = pageFiles.get(pageNum);
            String imageFile = files.get("image");
            String audioFile = files.get("audio");
            System.out.println("页面 " + pageNum + " 图片文件: " + imageFile + ", 音频文件: " + audioFile);

            if (imageFile != null) { // 只要有图片即可
                java.io.File imageFileObj = new java.io.File(tempDir, imageFile);
                java.io.File audioFileObj = audioFile != null ? new java.io.File(tempDir, audioFile) : null;

                if (imageFileObj.exists() && (audioFile == null || (audioFileObj != null && audioFileObj.exists()))) {
                    Map<String, Object> page = new HashMap<>();
                    page.put("pageNum", Integer.parseInt(pageNum));
                    page.put("imageUrl", tempDir + "/" + imageFile);
                    if (audioFile != null && audioFileObj.exists()) {
                        page.put("audioUrl", tempDir + "/" + audioFile);
                        Integer duration = parseAudioDuration(tempDir + "/" + audioFile);
                        page.put("duration", duration);
                        log.info("页面 {} 找到音频文件: {}, 时长: {}", pageNum, audioFile, duration);
                        // if (duration == null) {
                        //     warnings.add("页面 " + pageNum + " 的音频文件损坏或无法解析");
                        // }
                    } else {
                        page.put("audioUrl", null);
                        page.put("duration", null);
                        if (audioFile != null) {
                            log.warn("页面 {} 音频文件不存在: {}", pageNum, audioFile);
                        } else {
                            log.info("页面 {} 没有音频文件", pageNum);
                        }
                    }
                    validPages.add(page);
                } else {
                    if (!imageFileObj.exists()) {
                        warnings.add("页面 " + pageNum + " 缺少图片文件: " + imageFile);
                        log.error("页面 {} 图片文件不存在: {}", pageNum, imageFile);
                    }
                    if (audioFile != null && (audioFileObj == null || !audioFileObj.exists())) {
                        warnings.add("页面 " + pageNum + " 缺少音频文件: " + audioFile);
                        log.error("页面 {} 音频文件不存在: {}", pageNum, audioFile);
                    }
                }
            } else {
                warnings.add("页面 " + pageNum + " 缺少图片文件");
            }
        }
        
        // 检查是否有有效页面
        if (validPages.isEmpty()) {
            logError("压缩包内容异常：没有找到有效的页面文件", "N/A", new Exception("无有效页面"));
            return null;
        }
        
        // 按页码排序（这里已经是排序后的，但为了确保正确性再次排序）
        validPages.sort((a, b) -> Integer.compare((Integer) a.get("pageNum"), (Integer) b.get("pageNum")));
        
        result.put("validPages", validPages);
        result.put("totalPages", validPages.size());
        result.put("warnings", warnings);
        
        return result;
    }

    /**
     * 从文件名中提取页码
     */
    private String extractPageNumber(String fileName) {
        // 提取文件名（去掉路径）
        String simpleFileName = getFileNameFromPath(fileName);
        // 匹配格式：01.mp4, 01.mp3, 01.png, 01.jpg, 01.jpeg, 01.gif, 01.bmp, 01.webp
        // 以及音频格式：01.wav, 01.aac, 01.ogg, 01.flac
        Pattern pattern = Pattern.compile("^(\\d+)\\.(mp4|mp3|png|jpg|jpeg|gif|bmp|webp|wav|aac|ogg|flac)$", Pattern.CASE_INSENSITIVE);
        Matcher matcher = pattern.matcher(simpleFileName);
        if (matcher.find()) {
            return matcher.group(1);
        }
        return null;
    }

    /**
     * 获取文件类型
     */
    private String getFileType(String fileName) {
        String extension = getFileExtension(fileName);
        if (extension != null) {
            // 图片格式
            if ("mp4".equalsIgnoreCase(extension) || 
                "png".equalsIgnoreCase(extension) || 
                "jpg".equalsIgnoreCase(extension) || 
                "jpeg".equalsIgnoreCase(extension) || 
                "gif".equalsIgnoreCase(extension) || 
                "bmp".equalsIgnoreCase(extension) || 
                "webp".equalsIgnoreCase(extension)) {
                return "image";
            }
            // 音频格式
            else if ("mp3".equalsIgnoreCase(extension) || 
                     "MP3".equalsIgnoreCase(extension) ||
                     "wav".equalsIgnoreCase(extension) || 
                     "aac".equalsIgnoreCase(extension) || 
                     "ogg".equalsIgnoreCase(extension) || 
                     "flac".equalsIgnoreCase(extension)) {
                return "audio";
            }
        }
        return null;
    }

    /**
     * 清理临时目录
     */
    private void cleanupTempDirectory(String tempDir) {
        try {
            java.io.File tempDirFile = new java.io.File(tempDir);
            if (tempDirFile.exists()) {
                FileUtils.deleteDirectory(tempDirFile);
            }
        } catch (Exception e) {
            log.warn("清理临时目录失败: {}", tempDir, e);
        }
    }

    @Override
    public Integer parseAudioDuration(String audioFilePath) {
        try {
            // 这里应该使用ffmpeg工具解析音频文件
            // 由于没有ffmpeg依赖，这里返回模拟数据
            // 实际实现应该调用ffmpeg命令获取音频时长
            
            // 模拟返回音频时长（秒）
            // 可以根据文件大小或其他特征来估算时长
            byte[] audioContent = fileService.getFileContent(fileConfigService.getMasterFileClient().getId(), audioFilePath);
            if (audioContent != null) {
                // 简单的估算：假设音频文件大小与时长成正比
                // 这里使用一个简单的估算公式，实际应该使用专业的音频解析库
                int estimatedDuration = Math.max(10, audioContent.length / 16000); // 假设16KB/s的比特率
                return Math.min(estimatedDuration, 300); // 限制最大时长为5分钟
            }
            
            return 30; // 默认30秒
            
        } catch (Exception e) {
            logError("音频时长解析失败", "N/A", e);
            return null; // 返回null表示音频文件损坏或解析失败
        }
    }

    @Override
    public boolean transferFile(String sourcePath, String targetPath) {
        try {
            logOperation("开始文件转存", "N/A", "从 " + sourcePath + " 到 " + targetPath);
            
            // 1. 从MinIO临时路径下载文件
            byte[] fileContent = fileService.getFileContent(fileConfigService.getMasterFileClient().getId(), sourcePath);
            if (fileContent == null || fileContent.length == 0) {
                logError("文件转存失败：源文件为空", "N/A", new Exception("源文件为空"));
                return false;
            }
            
            // 2. 上传到MinIO永久路径
            String fileName = getFileNameFromPath(targetPath);
            String fileUrl = fileService.createFile(fileName, targetPath, fileContent);
            
            if (fileUrl == null || fileUrl.isEmpty()) {
                logError("文件转存失败：上传到目标路径失败", "N/A", new Exception("上传失败"));
                return false;
            }
            
            // 3. 删除临时文件（可选，这里暂时不删除，保留临时文件用于调试）
            // deleteTempFile(sourcePath);
            
            logOperation("文件转存成功", "N/A", targetPath);
            return true;
            
        } catch (Exception e) {
            logError("文件转存失败", "N/A", e);
            return false;
        }
    }

    @Override
    public void deleteTempFile(String filePath) {
        try {
            // 这里应该调用MinIO删除接口
            // 由于FileService没有直接的删除方法，这里暂时记录日志
            logOperation("删除临时文件", "N/A", filePath);
            
            // 实际实现应该调用MinIO的删除接口
            // 例如：minioClient.removeObject(RemoveObjectArgs.builder().bucket(MINIO_BUCKET).object(filePath).build());
            
        } catch (Exception e) {
            logError("删除临时文件失败", "N/A", e);
        }
    }

    /**
     * 删除临时文件（带返回值）
     *
     * @param filePath 文件路径
     * @return 是否删除成功
     */
    private boolean deleteTempFileWithResult(String filePath) {
        try {
            // 这里应该调用MinIO删除接口
            // 由于FileService没有直接的删除方法，这里暂时记录日志
            logOperation("删除临时文件", "N/A", filePath);
            
            // 实际实现应该调用MinIO的删除接口
            // 例如：minioClient.removeObject(RemoveObjectArgs.builder().bucket(MINIO_BUCKET).object(filePath).build());
            
            return true; // 模拟删除成功
            
        } catch (Exception e) {
            logError("删除临时文件失败", "N/A", e);
            return false;
        }
    }

    @Override
    public Map<String, Object> validateBookTitle(String title) {
        Map<String, Object> result = new HashMap<>();

        if (title == null || title.trim().isEmpty()) {
            result.put("valid", false);
            result.put("message", "绘本名称不能为空");
            return result;
        }

        if (title.length() > 100) {
            result.put("valid", false);
            result.put("message", "绘本名称长度不能超过100个字符");
            return result;
        }

        result.put("valid", true);
        result.put("message", "校验通过");
        return result;
    }

    @Override
    public boolean checkIdempotency(String sessionUuid) {
        String processedKey = buildProcessedSessionKey(sessionUuid);
        Boolean hasKey = stringRedisTemplate.hasKey(processedKey);
        
        if (Boolean.TRUE.equals(hasKey)) {
            logOperation("检测到幂等性：会话已处理", sessionUuid);
            return true;
        }
        
        // 检查是否正在处理中（防止并发提交）
        String processingKey = buildProcessingSessionKey(sessionUuid);
        Boolean isProcessing = stringRedisTemplate.hasKey(processingKey);
        
        if (Boolean.TRUE.equals(isProcessing)) {
            logOperation("检测到幂等性：会话正在处理中", sessionUuid);
            return true;
        }
        
        return false;
    }

    @Override
    public void markSessionAsProcessed(String sessionUuid) {
        String processedKey = buildProcessedSessionKey(sessionUuid);
        // 存储bookId而不是"processed"字符串
        stringRedisTemplate.opsForValue().set(processedKey, "processed", PROCESSED_EXPIRE_HOURS, TimeUnit.HOURS);
        logOperation("标记会话为已处理", sessionUuid);
    }

    /**
     * 标记会话为处理中（防止并发提交）
     *
     * @param sessionUuid 会话UUID
     * @return 是否成功标记
     */
    private boolean markSessionAsProcessing(String sessionUuid) {
        String processingKey = buildProcessingSessionKey(sessionUuid);
        
        // 使用SET NX命令，只有在键不存在时才设置
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(processingKey, "processing", 5, TimeUnit.MINUTES);
        
        if (Boolean.TRUE.equals(success)) {
            logOperation("标记会话为处理中", sessionUuid);
            return true;
        } else {
            logOperation("会话已在处理中，拒绝重复提交", sessionUuid);
            return false;
        }
    }

    /**
     * 清除处理中标记
     *
     * @param sessionUuid 会话UUID
     */
    private void clearProcessingMark(String sessionUuid) {
        String processingKey = buildProcessingSessionKey(sessionUuid);
        stringRedisTemplate.delete(processingKey);
        logOperation("清除处理中标记", sessionUuid);
    }

    /**
     * 构建处理中会话键
     *
     * @param sessionUuid 会话UUID
     * @return Redis键
     */
    private String buildProcessingSessionKey(String sessionUuid) {
        return "processing:" + sessionUuid;
    }

    /**
     * 验证文件完整性
     *
     * @param filePath 文件路径
     * @return 是否完整
     */
    private boolean validateFileIntegrity(String filePath) {
        try {
            byte[] fileContent = fileService.getFileContent(fileConfigService.getMasterFileClient().getId(), filePath);
            return fileContent != null && fileContent.length > 0;
        } catch (Exception e) {
            logError("文件完整性校验失败", "N/A", e);
            return false;
        }
    }

    /**
     * 验证压缩包格式
     *
     * @param filePath 文件路径
     * @return 是否有效
     */
    private boolean validateCompressedFileFormat(String filePath) {
        try {
            String fileName = getFileNameFromPath(filePath);
            String extension = getFileExtension(fileName);
            
            if (extension == null) {
                return false;
            }
            
            for (String allowedType : ALLOWED_COMPRESSED_TYPES) {
                if (allowedType.equalsIgnoreCase(extension)) {
                    return true;
                }
            }
            
            return false;
        } catch (Exception e) {
            logError("压缩包格式校验失败", "N/A", e);
            return false;
        }
    }

    /**
     * 记录详细的异常信息
     *
     * @param operation 操作名称
     * @param sessionUuid 会话UUID
     * @param exception 异常
     * @param additionalInfo 额外信息
     */
    private void logDetailedError(String operation, String sessionUuid, Exception exception, Object... additionalInfo) {
        StringBuilder sb = new StringBuilder();
        sb.append("操作: ").append(operation);
        sb.append(", 会话UUID: ").append(sessionUuid);
        sb.append(", 异常类型: ").append(exception.getClass().getSimpleName());
        sb.append(", 异常消息: ").append(exception.getMessage());
        
        if (additionalInfo.length > 0) {
            sb.append(", 额外信息: ").append(Arrays.toString(additionalInfo));
        }
        
        log.error(sb.toString(), exception);
    }

    // ========== 私有工具方法 ==========

    /**
     * 构建Redis键
     */
    private String buildSessionTempKeysKey(String sessionUuid) {
        return SESSION_TEMP_KEYS_PREFIX + sessionUuid + ":tempKeys";
    }

    /**
     * 构建已处理会话键
     */
    private String buildProcessedSessionKey(String sessionUuid) {
        return SESSION_PROCESSED_PREFIX + sessionUuid;
    }

    /**
     * 构建MinIO临时路径
     */
    private String buildTempPath(String sessionUuid, String filename) {
        return "books/temp/" + sessionUuid + "/" + filename;
    }

    /**
     * 构建MinIO永久路径
     */
    private String buildPermanentPath(Long bookId, String filename) {
        return "books/" + bookId + "/resources/" + filename;
    }

    /**
     * 构建封面永久路径
     */
    private String buildCoverPermanentPath(Long bookId) {
        return "books/" + bookId + "/cover/cover.jpg";
    }

    /**
     * 记录操作日志
     */
    private void logOperation(String operation, String sessionUuid, Object... params) {
        log.info("绘本上传操作 - {} - 会话UUID: {} - 参数: {}", operation, sessionUuid, Arrays.toString(params));
    }

    /**
     * 记录错误日志
     */
    private void logError(String operation, String sessionUuid, Throwable e) {
        log.error("绘本上传操作失败 - {} - 会话UUID: {} - 错误: {}", operation, sessionUuid, e.getMessage(), e);
    }

    /**
     * 生成签名
     */
    private String generateSign(String sessionUuid, String type, long timestamp) {
        String data = sessionUuid + type + timestamp + "yudao_secret_key";
        return UUID.nameUUIDFromBytes(data.getBytes()).toString().substring(0, 8);
    }

    /**
     * 获取文件扩展名
     */
    private String getFileExtension(String filename) {
        if (filename == null || filename.isEmpty()) {
            return null;
        }

        int lastDotIndex = filename.lastIndexOf('.');
        if (lastDotIndex == -1 || lastDotIndex == filename.length() - 1) {
            return null;
        }

        return filename.substring(lastDotIndex + 1).toLowerCase();
    }

    /**
     * 从路径中获取文件名
     */
    private String getFileNameFromPath(String path) {
        if (path == null || path.isEmpty()) {
            return "unknown";
        }

        int lastSlashIndex = path.lastIndexOf('/');
        if (lastSlashIndex == -1 || lastSlashIndex == path.length() - 1) {
            return path;
        }

        return path.substring(lastSlashIndex + 1);
    }

    /**
     * 扫描过期的临时目录
     *
     * @return 过期的会话UUID列表
     */
    private List<String> scanExpiredTempDirectories() {
        List<String> expiredSessionUuids = new ArrayList<>();
        
        try {
            // 由于无法直接访问MinIO的文件系统，这里使用模拟实现
            // 实际实现应该通过MinIO API扫描books/temp/目录
            
            // 扫描Redis中的tempKey，检查是否过期
            Set<String> keys = stringRedisTemplate.keys(SESSION_TEMP_KEYS_PREFIX + "*");
            if (keys != null) {
                int processedCount = 0;
                for (String key : keys) {
                    // 限制单次处理数量，避免影响性能
                    if (processedCount >= CLEANUP_MAX_SESSIONS) {
                        log.warn("达到单次清理最大会话数限制: {}", CLEANUP_MAX_SESSIONS);
                        break;
                    }
                    
                    // 提取会话UUID
                    String sessionUuid = extractSessionUuidFromKey(key);
                    if (sessionUuid != null) {
                        // 检查是否过期（通过Redis的TTL）
                        Long ttl = stringRedisTemplate.getExpire(key);
                        if (ttl != null && ttl <= 0) {
                            expiredSessionUuids.add(sessionUuid);
                        }
                        processedCount++;
                    }
                }
            }
            
            logOperation("扫描过期目录完成", "SYSTEM", 
                String.format("处理会话数: %d, 发现过期会话: %d", 
                    keys != null ? keys.size() : 0, expiredSessionUuids.size()));
            
        } catch (Exception e) {
            logError("扫描过期目录失败", "SYSTEM", e);
        }
        
        return expiredSessionUuids;
    }

    /**
     * 删除过期的临时文件
     *
     * @param expiredSessionUuids 过期的会话UUID列表
     * @return 删除的文件数量
     */
    private int deleteExpiredTempFiles(List<String> expiredSessionUuids) {
        int deletedCount = 0;
        int processedCount = 0;
        
        try {
            for (String sessionUuid : expiredSessionUuids) {
                // 获取该会话的所有文件路径
                Map<String, Object> tempKeyInfo = getTempKeyInfo(sessionUuid);
                if (tempKeyInfo != null) {
                    // 删除压缩包文件
                    String compressedPath = (String) tempKeyInfo.get("compressed_path");
                    if (compressedPath != null) {
                        if (deleteTempFileWithResult(compressedPath)) {
                            deletedCount++;
                        }
                    }
                    
                    // 删除封面文件
                    String coverPath = (String) tempKeyInfo.get("cover_path");
                    if (coverPath != null) {
                        if (deleteTempFileWithResult(coverPath)) {
                            deletedCount++;
                        }
                    }
                }
                
                processedCount++;
                
                // 批量处理，避免单次处理过多
                if (processedCount % CLEANUP_BATCH_SIZE == 0) {
                    logOperation("批量删除过期文件", "SYSTEM", 
                        String.format("已处理: %d, 已删除: %d", processedCount, deletedCount));
                }
            }
            
            logOperation("删除过期文件完成", "SYSTEM", 
                String.format("总处理: %d, 总删除: %d", processedCount, deletedCount));
            
        } catch (Exception e) {
            logError("删除过期文件失败", "SYSTEM", e);
        }
        
        return deletedCount;
    }

    /**
     * 清理过期的Redis数据
     *
     * @param expiredSessionUuids 过期的会话UUID列表
     * @return 清理的Redis键数量
     */
    private int cleanupExpiredRedisData(List<String> expiredSessionUuids) {
        int deletedCount = 0;
        int processedCount = 0;
        
        try {
            for (String sessionUuid : expiredSessionUuids) {
                // 删除tempKey信息
                String tempKeysKey = buildSessionTempKeysKey(sessionUuid);
                Boolean deleted = stringRedisTemplate.delete(tempKeysKey);
                if (Boolean.TRUE.equals(deleted)) {
                    deletedCount++;
                }
                
                // 删除已处理标记
                String processedKey = buildProcessedSessionKey(sessionUuid);
                Boolean deletedProcessed = stringRedisTemplate.delete(processedKey);
                if (Boolean.TRUE.equals(deletedProcessed)) {
                    deletedCount++;
                }
                
                processedCount++;
                
                // 批量处理，避免单次处理过多
                if (processedCount % CLEANUP_BATCH_SIZE == 0) {
                    logOperation("批量清理Redis数据", "SYSTEM", 
                        String.format("已处理: %d, 已删除: %d", processedCount, deletedCount));
                }
            }
            
            logOperation("清理Redis数据完成", "SYSTEM", 
                String.format("总处理: %d, 总删除: %d", processedCount, deletedCount));
            
        } catch (Exception e) {
            logError("清理Redis数据失败", "SYSTEM", e);
        }
        
        return deletedCount;
    }

    /**
     * 从Redis键中提取会话UUID
     *
     * @param key Redis键
     * @return 会话UUID
     */
    private String extractSessionUuidFromKey(String key) {
        if (key == null || !key.startsWith(SESSION_TEMP_KEYS_PREFIX)) {
            return null;
        }
        
        // 格式: session:{sessionUuid}:tempKeys
        String prefix = SESSION_TEMP_KEYS_PREFIX;
        String suffix = ":tempKeys";
        
        if (key.length() <= prefix.length() + suffix.length()) {
            return null;
        }
        
        String sessionUuid = key.substring(prefix.length(), key.length() - suffix.length());
        
        // 验证UUID格式
        try {
            UUID.fromString(sessionUuid);
            return sessionUuid;
        } catch (IllegalArgumentException e) {
            return null;
        }
    }

    /**
     * 带回滚功能的文件转存
     *
     * @param sourcePath 源文件路径
     * @param targetPath 目标文件路径
     * @param transferredFiles 已转存文件列表（用于回滚）
     * @return 是否转存成功
     */
    private boolean transferFileWithRollback(String sourcePath, String targetPath, List<String> transferredFiles) {
        try {
            logOperation("开始文件转存", "N/A", "从 " + sourcePath + " 到 " + targetPath);
            
            // 1. 从MinIO临时路径下载文件
            byte[] fileContent = fileService.getFileContent(fileConfigService.getMasterFileClient().getId(), sourcePath);
            if (fileContent == null || fileContent.length == 0) {
                logError("文件转存失败：源文件为空", "N/A", new Exception("源文件为空"));
                return false;
            }
            
            // 2. 上传到MinIO永久路径
            String fileName = getFileNameFromPath(targetPath);
            String fileUrl = fileService.createFile(fileName, targetPath, fileContent);
            
            if (fileUrl == null || fileUrl.isEmpty()) {
                logError("文件转存失败：上传到目标路径失败", "N/A", new Exception("上传失败"));
                return false;
            }
            
            // 3. 记录已转存文件（用于回滚）
            transferredFiles.add(targetPath);
            
            // 4. 删除源文件（从MinIO临时路径删除）
            try {
                FileClient client = fileConfigService.getMasterFileClient();
                client.delete(sourcePath);
                logOperation("删除源文件成功", "N/A", sourcePath);
            } catch (Exception e) {
                logError("删除源文件失败", "N/A", e);
                // 不因为删除源文件失败而影响转存结果
            }
            
            logOperation("文件转存成功", "N/A", targetPath);
            return true;
            
        } catch (Exception e) {
            logError("文件转存失败", "N/A", e);
            return false;
        }
    }

    /**
     * 处理提交失败的回滚
     *
     * @param bookId 绘本ID（可能为null）
     * @param transferredFiles 已转存的文件列表
     * @param sessionUuid 会话UUID
     * @param exception 异常信息
     */
    private void handleSubmitRollback(Long bookId, List<String> transferredFiles, String sessionUuid, Exception exception) {
        logOperation("开始异常回滚处理", sessionUuid, "bookId: " + bookId);
        
        try {
            // 1. 删除已转存的文件
            if (!transferredFiles.isEmpty()) {
                for (String filePath : transferredFiles) {
                    try {
                        deleteTempFileWithResult(filePath);
                        logOperation("回滚删除文件", sessionUuid, filePath);
                    } catch (Exception e) {
                        logError("回滚删除文件失败", sessionUuid, e);
                    }
                }
            }
            
            // 2. 删除数据库记录（如果已创建）
            if (bookId != null) {
                try {
                    // 删除页面记录
                    bookPageMapper.deleteByBookId(bookId);
                    // 删除绘本记录
                    bookMapper.deleteById(bookId);
                    logOperation("回滚删除数据库记录", sessionUuid, "bookId: " + bookId);
                } catch (Exception e) {
                    logError("回滚删除数据库记录失败", sessionUuid, e);
                }
            }
            
            // 3. 清理Redis数据
            try {
                deleteTempKeyInfo(sessionUuid);
                logOperation("回滚清理Redis数据", sessionUuid);
            } catch (Exception e) {
                logError("回滚清理Redis数据失败", sessionUuid, e);
            }
            
            // 4. 清理临时目录（如果存在）
            // 注意：这里需要从异常信息中获取tempDir，或者通过其他方式获取
            // 由于tempDir是局部变量，这里暂时不处理，在finally块中处理
            
            logOperation("异常回滚处理完成", sessionUuid);
            
        } catch (Exception e) {
            logError("异常回滚处理失败", sessionUuid, e);
        }
    }

    /**
     * 删除整个临时文件夹及其内容
     *
     * @param tempDir 临时目录路径
     * @return 是否删除成功
     */
    private boolean deleteTempDirectoryCompletely(String tempDir) {
        try {
            if (tempDir == null || tempDir.trim().isEmpty()) {
                log.warn("临时目录路径为空，跳过删除");
                return true;
            }
            
            java.io.File tempDirectory = new java.io.File(tempDir);
            if (!tempDirectory.exists()) {
                log.warn("临时目录不存在，跳过删除: {}", tempDir);
                return true;
            }
            
            logOperation("开始删除临时目录", "N/A", tempDir);
            
            // 递归删除目录及其内容
            boolean deleted = deleteDirectoryRecursively(tempDirectory);
            
            if (deleted) {
                logOperation("临时目录删除成功", "N/A", tempDir);
            } else {
                logError("临时目录删除失败", "N/A", new Exception("删除目录失败: " + tempDir));
            }
            
            return deleted;
            
        } catch (Exception e) {
            logError("删除临时目录异常", "N/A", e);
            return false;
        }
    }
    
    /**
     * 递归删除目录及其内容
     *
     * @param directory 要删除的目录
     * @return 是否删除成功
     */
    private boolean deleteDirectoryRecursively(java.io.File directory) {
        if (directory.isDirectory()) {
            java.io.File[] files = directory.listFiles();
            if (files != null) {
                for (java.io.File file : files) {
                    if (file.isDirectory()) {
                        deleteDirectoryRecursively(file);
                    } else {
                        if (!file.delete()) {
                            log.warn("删除文件失败: {}", file.getAbsolutePath());
                        }
                    }
                }
            }
        }
        return directory.delete();
    }

    /**
     * 将解压后的文件上传到MinIO正式目录
     *
     * @param tempFilePath 临时文件路径（本地临时目录）
     * @param targetPath 目标文件路径（MinIO永久路径）
     * @param transferredFiles 已转存文件列表（用于回滚）
     * @return 是否上传成功
     */
    private boolean uploadExtractedFileToMinio(String tempFilePath, String targetPath, List<String> transferredFiles) {
        try {
            logOperation("开始上传解压文件到MinIO", "N/A", "从 " + tempFilePath + " 到 " + targetPath);
            
            // 1. 从临时目录读取文件
            java.io.File tempFile = new java.io.File(tempFilePath);
            if (!tempFile.exists()) {
                logError("上传解压文件失败：临时文件不存在", "N/A", new Exception("临时文件不存在: " + tempFilePath));
                return false;
            }
            
            // 2. 读取文件内容
            byte[] fileContent = Files.readAllBytes(tempFile.toPath());
            if (fileContent.length == 0) {
                logError("上传解压文件失败：文件内容为空", "N/A", new Exception("文件内容为空"));
                return false;
            }
            
            // 3. 上传到MinIO永久路径
            String fileName = getFileNameFromPath(targetPath);
            String fileUrl = fileService.createFile(fileName, targetPath, fileContent);
            
            if (fileUrl == null || fileUrl.isEmpty()) {
                logError("上传解压文件失败：上传到目标路径失败", "N/A", new Exception("上传失败"));
                return false;
            }
            
            // 4. 记录已转存文件（用于回滚）
            transferredFiles.add(targetPath);
            
            // 5. 删除本地临时文件
            try {
                if (tempFile.delete()) {
                    logOperation("删除本地临时文件成功", "N/A", tempFilePath);
                } else {
                    logError("删除本地临时文件失败", "N/A", new Exception("删除文件失败: " + tempFilePath));
                }
            } catch (Exception e) {
                logError("删除本地临时文件异常", "N/A", e);
                // 不因为删除本地文件失败而影响上传结果
            }
            
            logOperation("上传解压文件成功", "N/A", targetPath);
            return true;
            
        } catch (Exception e) {
            logError("上传解压文件失败", "N/A", e);
            return false;
        }
    }
} 